/**
 * Captain
 * © 2017 Captain copyright，All rights reserved.
 * http://www.sccaptain.com.cn
 * 
 * JAVA : 8
 * 文  件  名: RequestBodyJackson2PageableArgumentResolver.java
 * 创  建  人: Roc Zheng (roc.djz@gmail.com)
 * 创建时间: 2017年7月6日 上午10:17:19
 * 版         本: 1.0.0
 * 备         注:
 * 修订历史:
 */
package com.material.c2c.web.bind;

import java.lang.reflect.Method;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableArgumentResolver;
import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.SortArgumentResolver;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.cpt.framework.web.bind.JsonRequestBody;
import com.cpt.framework.web.bind.RequestBodyJackson2ArgumentResolver;
import com.fasterxml.jackson.databind.JsonNode;
import com.material.c2c.web.util.SpringDataAnnotationUtils;

/**
 * 
 * 
 * @since 1.0
 * @version 1.0
 * @author Roc Zheng (roc.djz@gmail.com)
 */
public class RequestBodyJackson2PageableArgumentResolver extends RequestBodyJackson2ArgumentResolver implements PageableArgumentResolver {
  private static final String INVALID_DEFAULT_PAGE_SIZE = "方法  %s 配置的分页大小无效，值必须大于零。";
  private static final SortArgumentResolver DEFAULT_SORT_RESOLVER = new RequestBodyJackson2SortArgumentResolver();

  private static final String DEFAULT_PAGE_PARAMETER = "page";
  private static final String DEFAULT_SIZE_PARAMETER = "size";
  private static final String DEFAULT_PREFIX = "";
  private static final String DEFAULT_QUALIFIER_DELIMITER = "_";
  private static final int DEFAULT_MAX_PAGE_SIZE = 2000;
  static final Pageable DEFAULT_PAGE_REQUEST = new PageRequest(0, 20);

  private Pageable fallbackPageable = DEFAULT_PAGE_REQUEST;
  private SortArgumentResolver sortResolver;
  private String pageParameterName = DEFAULT_PAGE_PARAMETER;
  private String sizeParameterName = DEFAULT_SIZE_PARAMETER;
  private String prefix = DEFAULT_PREFIX;
  private String qualifierDelimiter = DEFAULT_QUALIFIER_DELIMITER;
  private int maxPageSize = DEFAULT_MAX_PAGE_SIZE;
  private boolean oneIndexedParameters = false;

  /**
   * Configures the {@link Pageable} to be used as fallback in case no {@link PageableDefault} or
   * {@link PageableDefaults} (the latter only supported in legacy mode) can be found at the method parameter to be
   * resolved.
   * <p>
   * If you set this to {@literal null}, be aware that you controller methods will get {@literal null} handed into them
   * in case no {@link Pageable} data can be found in the request. Note, that doing so will require you supply bot the
   * page <em>and</em> the size parameter with the requests as there will be no default for any of the parameters
   * available.
   * 
   * @param fallbackPageable the {@link Pageable} to be used as general fallback.
   */
  public void setFallbackPageable(Pageable fallbackPageable) {
    this.fallbackPageable = fallbackPageable;
  }

  /**
   * Returns whether the given {@link Pageable} is the fallback one.
   * 
   * @param pageable can be {@literal null}.
   * @return
   */
  public boolean isFallbackPageable(Pageable pageable) {
    return fallbackPageable == null ? false : fallbackPageable.equals(pageable);
  }

  /**
   * Configures the maximum page size to be accepted. This allows to put an upper boundary of the page size to prevent
   * potential attacks trying to issue an {@link OutOfMemoryError}. Defaults to {@link #DEFAULT_MAX_PAGE_SIZE}.
   * 
   * @param maxPageSize the maxPageSize to set
   * 
   * @since 1.0
   * @version 1.0
   * @author Roc Zheng (roc.djz@gmail.com)
   */
  public void setMaxPageSize(int maxPageSize) {
    this.maxPageSize = maxPageSize;
  }

  /**
   * Retrieves the maximum page size to be accepted. This allows to put an upper boundary of the page size to prevent
   * potential attacks trying to issue an {@link OutOfMemoryError}. Defaults to {@link #DEFAULT_MAX_PAGE_SIZE}.
   * 
   * @return the maximum page size allowed.
   * 
   * @since 1.0
   * @version 1.0
   * @author Roc Zheng (roc.djz@gmail.com)
   */
  protected int getMaxPageSize() {
    return this.maxPageSize;
  }

  /**
   * Configures the parameter name to be used to find the page number in the request. Defaults to {@code page}.
   * 
   * @param pageParameterName the parameter name to be used, must not be {@literal null} or empty.
   * 
   * @since 1.0
   * @version 1.0
   * @author Roc Zheng (roc.djz@gmail.com)
   */
  public void setPageParameterName(String pageParameterName) {
    Assert.hasText(pageParameterName, "pageParameterName 不能为空。");
    this.pageParameterName = pageParameterName;
  }

  /**
   * Retrieves the parameter name to be used to find the page number in the request. Defaults to {@code page}.
   * 
   * @return the parameter name to be used, never {@literal null} or empty.
   * 
   * @since 1.0
   * @version 1.0
   * @author Roc Zheng (roc.djz@gmail.com)
   */
  protected String getPageParameterName() {
    return this.pageParameterName;
  }

  /**
   * Configures the parameter name to be used to find the page size in the request. Defaults to {@code size}.
   * 
   * @param sizeParameterName the parameter name to be used, must not be {@literal null} or empty.
   * 
   * @since 1.0
   * @version 1.0
   * @author Roc Zheng (roc.djz@gmail.com)
   */
  public void setSizeParameterName(String sizeParameterName) {
    Assert.hasText(sizeParameterName, "sizeParameterNanme 不能为空。");
    this.sizeParameterName = sizeParameterName;
  }

  /**
   * Retrieves the parameter name to be used to find the page size in the request. Defaults to {@code size}.
   * 
   * @return the parameter name to be used, never {@literal null} or empty.
   */
  protected String getSizeParameterName() {
    return this.sizeParameterName;
  }

  /**
   * Configures a general prefix to be prepended to the page number and page size parameters. Useful to namespace the
   * property names used in case they are clashing with ones used by your application. By default, no prefix is used.
   * 
   * @param prefix the prefix to be used or {@literal null} to reset to the default.
   * 
   * @since 1.0
   * @version 1.0
   * @author Roc Zheng (roc.djz@gmail.com)
   */
  public void setPrefix(String prefix) {
    this.prefix = prefix == null ? DEFAULT_PREFIX : prefix;
  }

  /**
   * The delimiter to be used between the qualifier and the actual page number and size properties. Defaults to
   * {@code _}. So a qualifier of {@code foo} will result in a page number parameter of {@code foo_page}.
   * 
   * @param qualifierDelimiter the delimter to be used or {@literal null} to reset to the default.
   * 
   * @since 1.0
   * @version 1.0
   * @author Roc Zheng (roc.djz@gmail.com)
   */
  public void setQualifierDelimiter(String qualifierDelimiter) {
    this.qualifierDelimiter = qualifierDelimiter == null ? DEFAULT_QUALIFIER_DELIMITER : qualifierDelimiter;
  }

  /**
   * Configures whether to expose and assume 1-based page number indexes in the request parameters. Defaults to
   * {@literal false}, meaning a page number of 0 in the request equals the first page. If this is set to
   * {@literal true}, a page number of 1 in the request will be considered the first page.
   * 
   * @param oneIndexedParameters the oneIndexedParameters to set
   * 
   * @since 1.0
   * @version 1.0
   * @author Roc Zheng (roc.djz@gmail.com)
   */
  public void setOneIndexedParameters(boolean oneIndexedParameters) {
    this.oneIndexedParameters = oneIndexedParameters;
  }

  /**
   * Indicates whether to expose and assume 1-based page number indexes in the request parameters. Defaults to
   * {@literal false}, meaning a page number of 0 in the request equals the first page. If this is set to
   * {@literal true}, a page number of 1 in the request will be considered the first page.
   * 
   * @return whether to assume 1-based page number indexes in the request parameters.
   * 
   * @since 1.0
   * @version 1.0
   * @author Roc Zheng (roc.djz@gmail.com)
   */
  protected boolean isOneIndexedParameters() {
    return this.oneIndexedParameters;
  }
  
  /**
   * 初始化 {@link RequestBodyJackson2PageableArgumentResolver} 类的新实例。
   */
  public RequestBodyJackson2PageableArgumentResolver() {
    this(null);
  }
  
  /**
   * 初始化 {@link RequestBodyJackson2PageableArgumentResolver} 类的新实例。
   * @param sortResolver
   */
  public RequestBodyJackson2PageableArgumentResolver(SortArgumentResolver sortResolver) {
    this.sortResolver = sortResolver == null ? DEFAULT_SORT_RESOLVER : sortResolver;
  }
  
  /**
   * {@inheritDoc}
   */
  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    return Pageable.class.equals(parameter.getParameterType()) && super.supportsParameter(parameter);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Pageable resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
    SpringDataAnnotationUtils.assertPageableUniqueness(parameter);
    Pageable defaultOrFallback = getDefaultFromAnnotationOrFallback(parameter);
    
    JsonNode json = readJsonRequestBody(parameter, webRequest, binderFactory);
    if (null == json) {
      return null;
    }
    
    int page = Integer.MIN_VALUE;
    int pageSize = Integer.MIN_VALUE;
    boolean pageAndSizeGiven = false;
    if (json.has(pageParameterName) && json.has(sizeParameterName)) {
      pageAndSizeGiven = true;
      page = applyBoundaries(json.get(pageParameterName).asInt(Integer.MIN_VALUE), Integer.MAX_VALUE, true);
      pageSize = applyBoundaries(json.get(sizeParameterName).asInt(Integer.MIN_VALUE), maxPageSize, false);
    }
    
    JsonNode targetJson = json.path(getJsonPath(parameter)); 
    if (null != targetJson) {
      json = targetJson;
    }
    
    if (json.has(pageParameterName) && json.has(sizeParameterName)) {
      pageAndSizeGiven = true;
      page = applyBoundaries(json.get(pageParameterName).asInt(Integer.MIN_VALUE), Integer.MAX_VALUE, true);
      pageSize = applyBoundaries(json.get(sizeParameterName).asInt(Integer.MIN_VALUE), maxPageSize, false);
    }
    
    if (!pageAndSizeGiven && defaultOrFallback == null) {
      return null;
    }
    
    if (!pageAndSizeGiven) {
      page = defaultOrFallback.getPageNumber();
      pageSize = defaultOrFallback.getPageSize();
    }

    // Limit lower bound
    pageSize = pageSize < 1 ? defaultOrFallback.getPageSize() : pageSize;
    // Limit upper bound
    pageSize = pageSize > maxPageSize ? maxPageSize : pageSize;

    Sort sort = sortResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);

    // Default if necessary and default configured
    sort = sort == null && defaultOrFallback != null ? defaultOrFallback.getSort() : sort;

    return new PageRequest(page, pageSize, sort);
  }

  /**
   * Returns the name of the request parameter to find the {@link Pageable} information in. Inspects the given
   * {@link MethodParameter} for {@link Qualifier} present and prefixes the given source parameter name with it.
   * 
   * @param source the basic parameter name.
   * @param parameter the {@link MethodParameter} potentially qualified.
   * @return the name of the request parameter.
   */
  protected String getParameterNameToUse(String source, MethodParameter parameter) {
    StringBuilder builder = new StringBuilder(prefix);
    if (parameter != null && parameter.hasParameterAnnotation(Qualifier.class)) {
      builder.append(parameter.getParameterAnnotation(Qualifier.class).value());
      builder.append(qualifierDelimiter);
    }

    return builder.append(source).toString();
  }

  private Pageable getDefaultFromAnnotationOrFallback(MethodParameter methodParameter) {
    if (methodParameter.hasParameterAnnotation(PageableDefault.class)) {
      return getDefaultPageRequestFrom(methodParameter);
    }

    return fallbackPageable;
  }

  private static Pageable getDefaultPageRequestFrom(MethodParameter parameter) {
    PageableDefault defaults = parameter.getParameterAnnotation(PageableDefault.class);

    Integer defaultPageNumber = defaults.page();
    Integer defaultPageSize = SpringDataAnnotationUtils.getSpecificPropertyOrDefaultFromValue(defaults, "size");

    if (defaultPageSize < 1) {
      Method annotatedMethod = parameter.getMethod();
      throw new IllegalStateException(String.format(INVALID_DEFAULT_PAGE_SIZE, annotatedMethod));
    }

    if (defaults.sort().length == 0) {
      return new PageRequest(defaultPageNumber, defaultPageSize);
    }

    return new PageRequest(defaultPageNumber, defaultPageSize, defaults.direction(), defaults.sort());
  }
  
  private int applyBoundaries(int parameter, int upper, boolean shiftIndex) {
    int parsed = parameter - (oneIndexedParameters && shiftIndex ? 1 : 0);
    return parsed < 0 ? 0 : parsed > upper ? upper : parsed;
  }
  
  private String getJsonPath(MethodParameter parameter) {
    String path = parameter.getParameterName();
    if (parameter.hasParameterAnnotation(JsonRequestBody.class)) {
      JsonRequestBody jsonPathRequestBody = parameter.getParameterAnnotation(JsonRequestBody.class);
      if (StringUtils.hasText(jsonPathRequestBody.path())) {
        path = jsonPathRequestBody.path();
      }
    }
    return path;
  }
}
